/*
 * Copyright DataStax, Inc. and/or The Stargate Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package io.stargate.db.query.builder;

import com.datastax.oss.driver.shaded.guava.common.base.Preconditions;
import io.stargate.db.query.TypedValue;
import java.util.Objects;
import javax.annotation.Nullable;

/** A concrete value, or a bind marker. */
public abstract class Value<T> {
  private int internalIndex = -1;

  public static <T> Value<T> marker() {
    return new Marker<>();
  }

  public static <T> Value<T> of(T value) {
    return new Concrete<>(value);
  }

  public abstract boolean isMarker();

  public abstract T get();

  void setInternalIndex(int index) {
    Preconditions.checkArgument(index >= 0);
    this.internalIndex = index;
  }

  /**
   * The query build internally prepare every value and pass them as bound values. This is the index
   * of the marker in the query internally generated by the builder.
   *
   * @return a positive integer corresponding to the index of the marker in the internal query that
   *     this is the value of, or a negative integer if this value is not associated to a marker in
   *     the internal query.
   */
  int internalIndex() {
    return internalIndex;
  }

  @Override
  public boolean equals(Object obj) {
    if (!(obj instanceof Value)) return false;

    Value<?> that = (Value<?>) obj;
    if (this.isMarker() != that.isMarker()) return false;
    return this.isMarker() || Objects.equals(this.get(), that.get());
  }

  @Override
  public int hashCode() {
    return Objects.hash(isMarker(), isMarker() ? null : get());
  }

  private static class Concrete<T> extends Value<T> {
    private final @Nullable T value;

    private Concrete(T value) {
      // Explicitly using an UNSET value kind of don't make sense: just don't include whatever
      // you're setting to this value in the statement in the first place. UNSET is only truly
      // useful for prepared statements/bound values. So make sure no-one pass it here thinking this
      // may work, as it doesn't (we "could" make it work technically, as the query builder actually
      // prepare all values, even the ones that set at the time of the query construction. It adds
      // unnecessary complexity though).
      Preconditions.checkArgument(
          value != TypedValue.UNSET,
          "Cannot set a column to <unset>. <unset> should only be used when binding value to prepared bind marker");
      this.value = value;
    }

    @Override
    public boolean isMarker() {
      return false;
    }

    @Override
    public @Nullable T get() {
      return value;
    }

    @Override
    public String toString() {
      return value == null ? "null" : value.toString();
    }
  }

  static class Marker<T> extends Value<T> {
    private int externalIndex = -1;

    private Marker() {}

    void setExternalIndex(int externalIndex) {
      this.externalIndex = externalIndex;
    }

    /** Index of the marker, as an "external" (to the query building) marker. */
    int externalIndex() {
      Preconditions.checkState(externalIndex >= 0, "Marker has not bet set properly");
      return externalIndex;
    }

    @Override
    public boolean isMarker() {
      return true;
    }

    @Override
    public T get() {
      throw new UnsupportedOperationException("Cannot get the value of a marker");
    }

    @Override
    public String toString() {
      return "?";
    }
  }
}
